TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { notFound } from "next/navigation";3import { ArrowLeft } from "lucide-react";4import { requireAdmin } from "@/lib/session";5import { getUserDetail } from "@/lib/queries/admin";6import { formatDate, formatNumber, truncate } from "@/lib/format";7import { PageHeader } from "@/components/ui/page-header";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { Alert } from "@/components/ui/alert";13import { CopyButton } from "@/components/ui/copy-button";14import { DateCell, KV, Mono, Panel, PlanBadge } from "@/components/admin/primitives";15import { UserActions } from "@/components/admin/user-actions";1617export const dynamic = "force-dynamic";1819export default async function AdminUserDetail({ params }: { params: Promise<{ id: string }> }) {20 const admin = await requireAdmin();21 const { id } = await params;22 const d = await getUserDetail(id);23 if (!d) notFound();24 const { user } = d;2526 return (27 <>28 <div className="mb-4">29 <Button asChild variant="ghost" size="sm" className="-ml-2">30 <Link href="/admin/users">31 <ArrowLeft className="size-3.5" /> Users32 </Link>33 </Button>34 </div>35 <PageHeader36 eyebrow="User"37 title={38 <span className="flex flex-wrap items-center gap-2">39 {user.email}40 {user.role === "admin" ? <Badge variant="danger">admin</Badge> : null}41 {user.banned ? <Badge variant="danger" dot>banned</Badge> : null}42 {user.emailVerified ? <Badge variant="success" dot>verified</Badge> : <Badge variant="outline">unverified</Badge>}43 </span>44 }45 description={46 <span className="inline-flex items-center gap-1">47 <Mono>{user.id}</Mono>48 <CopyButton value={user.id} size="icon-sm" className="size-6" />49 </span>50 }51 actions={<UserActions userId={user.id} banned={user.banned} role={user.role} emailVerified={user.emailVerified} isSelf={user.id === admin.id} />}52 />5354 {user.banned ? (55 <Alert variant="danger" title="This account is banned" className="mb-4">56 Reason: {user.banReason || "not recorded"}57 </Alert>58 ) : null}5960 <StatGrid cols={4} className="mb-6">61 <Stat label="Organizations" value={formatNumber(d.memberships.length)} />62 <Stat label="Projects" value={formatNumber(d.projectsCount)} />63 <Stat label="API keys" value={formatNumber(d.keysCount)} hint="incl. revoked" />64 <Stat label="Requests (30d)" value={formatNumber(d.requests30d)} hint="across their organizations" />65 </StatGrid>6667 <div className="grid gap-6 lg:grid-cols-2">68 <Panel title="Profile">69 <KV70 items={[71 { label: "Name", value: user.name || "—" },72 { label: "Email", value: user.email },73 { label: "Role", value: user.role },74 { label: "Email verified", value: user.emailVerified ? "yes" : "no" },75 { label: "Onboarding completed", value: user.onboardingCompletedAt ? formatDate(user.onboardingCompletedAt) : "not yet" },76 { label: "Created", value: formatDate(user.createdAt) },77 { label: "Updated", value: formatDate(user.updatedAt) },78 { label: "Image", value: user.image ? truncate(user.image, 40) : "—", mono: true },79 ]}80 />81 </Panel>82 <Panel title="Organizations" flush>83 <Table>84 <TableHeader>85 <TableRow>86 <TableHead>Organization</TableHead>87 <TableHead>Plan</TableHead>88 <TableHead>Role</TableHead>89 <TableHead>Status</TableHead>90 <TableHead className="text-right">Joined</TableHead>91 </TableRow>92 </TableHeader>93 <TableBody>94 {d.memberships.length === 0 ? (95 <TableEmpty colSpan={5}>No memberships.</TableEmpty>96 ) : (97 d.memberships.map((m) => (98 <TableRow key={m.organizationId}>99 <TableCell>100 <Link href={`/admin/organizations/${m.organizationId}`} className="font-medium hover:underline">101 {m.name}102 </Link>103 </TableCell>104 <TableCell>105 <PlanBadge plan={m.plan} />106 </TableCell>107 <TableCell className="text-fg-muted">{m.role}</TableCell>108 <TableCell>{m.suspended ? <StatusBadge status="suspended" /> : <StatusBadge status="active" />}</TableCell>109 <TableCell className="text-right">110 <DateCell value={m.joinedAt} />111 </TableCell>112 </TableRow>113 ))114 )}115 </TableBody>116 </Table>117 </Panel>118 </div>119120 <Panel title="Sessions" description="Most recent 10 login sessions." flush className="mt-6">121 <Table>122 <TableHeader>123 <TableRow>124 <TableHead>Created</TableHead>125 <TableHead>Expires</TableHead>126 <TableHead>IP</TableHead>127 <TableHead>User agent</TableHead>128 </TableRow>129 </TableHeader>130 <TableBody>131 {d.sessions.length === 0 ? (132 <TableEmpty colSpan={4}>Never logged in.</TableEmpty>133 ) : (134 d.sessions.map((s) => (135 <TableRow key={s.id}>136 <TableCell>137 <DateCell value={s.createdAt} relative={false} />138 </TableCell>139 <TableCell>140 <DateCell value={s.expiresAt} />141 </TableCell>142 <TableCell>143 <Mono>{s.ipAddress ?? "—"}</Mono>144 </TableCell>145 <TableCell className="max-w-[420px] truncate text-[12.5px] text-fg-muted" title={s.userAgent ?? undefined}>146 {s.userAgent ?? "—"}147 </TableCell>148 </TableRow>149 ))150 )}151 </TableBody>152 </Table>153 </Panel>154155 <Panel title="Audit trail" description="Last 50 audit entries where this user is the actor." flush className="mt-6">156 <Table>157 <TableHeader>158 <TableRow>159 <TableHead>When</TableHead>160 <TableHead>Action</TableHead>161 <TableHead>Target</TableHead>162 <TableHead>Metadata</TableHead>163 <TableHead>IP</TableHead>164 </TableRow>165 </TableHeader>166 <TableBody>167 {d.audit.length === 0 ? (168 <TableEmpty colSpan={5}>No audit entries.</TableEmpty>169 ) : (170 d.audit.map((a) => (171 <TableRow key={a.id}>172 <TableCell>173 <DateCell value={a.createdAt} relative={false} />174 </TableCell>175 <TableCell>176 <Badge variant={a.action === "admin.action" ? "danger" : "default"}>{a.action}</Badge>177 </TableCell>178 <TableCell>179 <Mono>{a.target ?? "—"}</Mono>180 </TableCell>181 <TableCell className="max-w-[360px] truncate font-mono text-[12px] text-fg-muted" title={a.metadata ? JSON.stringify(a.metadata) : undefined}>182 {a.metadata ? JSON.stringify(a.metadata) : "—"}183 </TableCell>184 <TableCell>185 <Mono>{a.ipAddress ?? "—"}</Mono>186 </TableCell>187 </TableRow>188 ))189 )}190 </TableBody>191 </Table>192 </Panel>193 </>194 );195}196